/*
 * Copyright 2002-2006 the original author or authors. Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language governing permissions and limitations under the
 * License.
 */
package pikes.xml;

import java.io.IOException;

import pikes.core.Printable;
import pikes.core.Printer;
import pikes.util.Assert;

/**
 * XML printer which helps printing the XML content formatted. All new XML content is printed in new line. The following
 * parameter can be set in the constructors:
 * <ul>
 * <li>indentation can be turned on and off (off by default)</li>
 * <li>if 'tab' or 'space' should be used as indentation character ('space' by default)</li>
 * <li>how many space characters should be used for tabbing(8 by default)</li>
 * </ul>
 * @author Peter Bona
 */

public class XMLFormatter implements XMLVisitor {

	/**
	 * Default number of spaces.
	 */
	public static final int DEFAULT_NUMBER_OF_SPACES = 8;

	private XMLWriter xmlPrinter = null;

	private Printer typeWriter = null;

	private boolean newLine = false;

	private boolean indent = false;

	private long tagLevel = 0;

	private boolean useTab = false;

	private int numberOfSpace = DEFAULT_NUMBER_OF_SPACES;

	/**
	 * Creates a XML formatter which takes character stream and XMLPrinter.
	 * @param xmlPrinter used for delegating the actual printing of the XML markups
	 */
	public XMLFormatter(final XMLWriter xmlPrinter) {
		Assert.notNull(xmlPrinter);
		this.xmlPrinter = xmlPrinter;
	}

	public XMLFormatter(final Printer typeWriter) {
		this(new XMLWriter(typeWriter));
		this.typeWriter = typeWriter;
	}

	public XMLFormatter(final Printer typeWriter, final boolean shouldExpandEmptyTag) {
		this(new XMLWriter(typeWriter, shouldExpandEmptyTag));
		this.typeWriter = typeWriter;
	}

	/**
	 * Creates a XML formatter which takes character stream and XMLPrinter.
	 * @param printer used for delegating the actual printing of the XML markups
	 */
	public XMLFormatter(final Printer printer, final boolean shouldExpandEmptyTag, final boolean newLine) {
		this(printer, shouldExpandEmptyTag);
		this.newLine = newLine;
	}

	/**
	 * Creates a XML formatter which takes character stream, XMLPrinter and boolean if the indentation should be turned
	 * on or off.
	 * @param xmlPrinter used for delegating the actual printing of the XML markups
	 * @param indent indentation will be used, if true
	 */
	public XMLFormatter(final Printer xmlPrinter, final boolean shouldExpandEmptyTag, final boolean newLine,
			final boolean indent) {
		this(xmlPrinter, shouldExpandEmptyTag, newLine);
		this.indent = indent;
	}

	/**
	 * Delegates the call to underlying XMLPrinter
	 * @throws XMLVisitingException if the same exception is thrown in XMLPrinter
	 */
	public final void attribute(CharSequence name, CharSequence value) throws XMLVisitingException {
		xmlPrinter.attribute(name, value);
	}

	public final void attribute(CharSequence name, Printable value) throws XMLVisitingException {
		xmlPrinter.attribute(name, value);
	}

	/**
	 * Starts a new line and prints indenting characters, if set, then delegates the call to underlying XMLPrinter
	 * @throws XMLVisitingException if the same exception is thrown in XMLPrinter
	 */
	public final void text(CharSequence text) throws XMLVisitingException {
		newLine();
		indent();
		xmlPrinter.text(text);
	}

	public final void text(Printable printable) throws XMLVisitingException {
		newLine();
		indent();
		xmlPrinter.text(new FormattedPrintable(printable, newLine, indent, tagLevel, useTab, numberOfSpace));
	}

	/**
	 * Starts a new line and prints indenting characters, if set, then delegates the call to underlying XMLPrinter
	 * @throws XMLVisitingException if the same exception is thrown in XMLPrinter
	 */
	public final void closeSecondTag(CharSequence name) throws XMLVisitingException {
		tagLevel--;
		newLine();
		indent();
		xmlPrinter.closeSecondTag(name);
	}

	/**
	 * Delegates the call to underlying XMLPrinter
	 * @throws XMLVisitingException if the same exception is thrown in XMLPrinter
	 */
	public final void closingFirstTag(CharSequence name, boolean hasChildren) throws XMLVisitingException {
		if (hasChildren) {
			tagLevel++;
		}
		xmlPrinter.closingFirstTag(name, hasChildren);
	}

	/**
	 * Starts a new line and prints indenting characters, if set, then delegates the call to underlying XMLPrinter
	 * @throws XMLVisitingException if the same exception is thrown in XMLPrinter
	 */
	public final void openingFirstTag(CharSequence name) throws XMLVisitingException {
		beforeNewTag();
		xmlPrinter.openingFirstTag(name);
	}

	/**
	 * Starts a new line and prints indenting characters, if set, then delegates the call to underlying XMLPrinter
	 * @throws XMLVisitingException if the same exception is thrown in XMLPrinter
	 */
	public final void openingFirstTag(CharSequence prefix, CharSequence name) throws XMLVisitingException {
		beforeNewTag();
		xmlPrinter.openingFirstTag(prefix, name);
	}

	private void beforeNewTag() throws XMLVisitingException {
		newLine();
		indent();
	}

	/**
	 * Starts a new line and then delegates the call to underlying XMLPrinter
	 * @throws XMLVisitingException if the same exception is thrown in XMLPrinter
	 */
	public final void publicDocType(CharSequence name, CharSequence id, CharSequence uri) throws XMLVisitingException {
		newLine();
		xmlPrinter.publicDocType(name, id, uri);

	}

	/**
	 * Delegates the call to underlying XMLPrinter
	 * @throws XMLVisitingException if the same exception is thrown in XMLPrinter
	 */
	public final void xmlDeclaration(CharSequence version) throws XMLVisitingException {
		xmlPrinter.xmlDeclaration(version);
	}

	/**
	 * Delegates the call to underlying XMLPrinter
	 * @throws XMLVisitingException if the same exception is thrown in XMLPrinter
	 */
	public final void xmlDeclaration(CharSequence version, CharSequence encoding) throws XMLVisitingException {
		xmlPrinter.xmlDeclaration(version, encoding);
	}

	/**
	 * Prints '\n'
	 * @throws XMLVisitingException if an IOException is thrown on the underlying Appendable
	 */
	private void newLine() throws XMLVisitingException {
		if (newLine) {
			typeWriter.newLine();
		}
	}

	/**
	 * Prints '\t' or ' ' characters depending on <code>useTab</code> property
	 * @throws XMLVisitingException if an IOException is thrown on the underlying Appendable
	 */
	private void indent() throws XMLVisitingException {
		if (indent) {
			for (int i = 0; i < tagLevel; i++) {
				typeWriter.tabulator();
			}
		}
	}

	/**
	 * Delegates the call to underlying XMLPrinter
	 * @throws XMLVisitingException if the same exception is thrown in XMLPrinter
	 */
	public final void comment(CharSequence comment) throws XMLVisitingException {
		xmlPrinter.comment(comment);
	}
}

class FormattedPrintable implements Printable, Appendable {

	private Printable delegate = null;

	private Printer printer = null;

	private boolean newLine = false;

	private boolean indent = false;

	private long tagLevel = 0;

	private boolean useTab = false;

	private int numberOfSpace = Printer.DEFAULT_NUMBER_OF_SPACES;

	public FormattedPrintable(final Printable delegate, final boolean newLine, final boolean indent,
			final long tagLevel, final boolean useTab, final int numberOfSpaces) {
		this.delegate = delegate;
		this.newLine = newLine;
		this.indent = indent;
		this.tagLevel = tagLevel;
		this.useTab = useTab;
		this.numberOfSpace = numberOfSpaces;

	}

	public final void print(Appendable appendable) throws IOException {
		this.printer = new Printer(appendable, useTab, numberOfSpace);
		delegate.print(this);

	}

	public final Appendable append(CharSequence csq) throws IOException {
		return append(csq, 0, csq.length());
	}

	public final Appendable append(char c) throws IOException {
		if (c == '\n') {
			if (newLine) {
				printer.newLine();
				if (indent) {
					for (int i = 0; i < tagLevel; i++) {
						printer.tabulator();
					}
				}
			}
		}
		else {
			printer.print(c);
		}

		return this;
	}

	public final Appendable append(CharSequence csq, int start, int end) throws IOException {
		for (int i = start; i < end; i++) {
			append(csq.charAt(i));
		}
		return this;
	}

}
